﻿module BoardTool.Board

open System
open System.Collections.Generic

open Tokenize

type Terrain =
    {
    Type : string
    mutable Level : int
    mutable Exits : int option
    }

let private TERRAIN_PARAMS_SEPARATOR_CHARS = [| ':' |]

let parseTerrain (terr : string) =
    let parts = terr.Split( TERRAIN_PARAMS_SEPARATOR_CHARS )
    {
    Type = parts.[0]
    Level = if parts.Length > 1 then int( parts.[1] ) else 0 // XXX 
    Exits = if parts.Length > 2 then Some <| int( parts.[2] ) else None
    }

let unparseTerrain (pt : Terrain) =
    let sb = Text.StringBuilder()
    sb.Append( pt.Type ).Append( ":" ).Append( pt.Level ) |> ignore
    if Option.isSome pt.Exits then
        sb.Append( ":" ).Append( Option.get pt.Exits ) |> ignore
    sb.ToString()

let mkTerrain tp lvl exOpt =
    { Type = tp; Level = lvl; Exits = exOpt }

let mkTerrainWithLevel tp lvl =
    { Type = tp; Level = lvl; Exits = None }

let mkTerrainWithExits tp lvl ex =
    { Type = tp; Level = lvl; Exits = Some ex }
    
type Hex =
    {
    Coords : int
    Elevation : int
    Terrains : Terrain list
    Theme : string
    }

let private LINE_SEPARATOR_CHARS = [| '\n' |]
let private TERRAIN_TYPE_SEPARATOR_CHARS = [| ';' |] 

type BoardFileEntry =
    | Size of int * int
    | Option of string * string
    | Hex of Hex
    | End
    | Unrecognized of string

let parseBoardFileEntry (line : string) =
    let line = line.Trim()
    match tokenize line with
    | [| "size"; sXSize; sYSize |] ->
        let xSize = int(sXSize)
        let ySize = int(sYSize) 
        Size( xSize, ySize )
    | [| "option"; sName; sValue |] ->
        Option( sName, sValue )
    | [| "hex"; sCoords; sElev; sTerrains; sTheme |] ->
        let coords = int(sCoords)
        let elev = int(sElev)
        let terrains = 
            sTerrains.Split( TERRAIN_TYPE_SEPARATOR_CHARS ) 
            |> Array.toList
            |> List.filter (fun s -> String.length s > 0)
            |> List.map parseTerrain
        Hex { Coords = coords; Elevation = elev; Terrains = terrains; Theme = sTheme }
    | [| "end" |] ->
        End
    | _ ->
        Unrecognized line

let unparseBoardFileEntry (writer : IO.TextWriter) (bfe : BoardFileEntry) =
    match bfe with
    | Size (xSize, ySize) -> 
        fprintfn writer "size %d %d" xSize ySize
    | Option (name, value) ->
        fprintfn writer "option %s %s" name value
    | Hex hd ->
        let terrains = hd.Terrains |> Seq.map unparseTerrain |> String.concat ";" 
        fprintfn writer "hex %d %d \"%s\" \"%s\"" hd.Coords hd.Elevation terrains hd.Theme
    | End ->
        fprintfn writer "end"
    | Unrecognized s ->
        fprintfn writer "%s" s

let unparseHex (hex : Hex) =
    let terrains = hex.Terrains |> Seq.map unparseTerrain |> String.concat ";"
    sprintf "hex %d %d \"%s\" \"%s\"" hex.Coords hex.Elevation terrains hex.Theme

let parseBoardEntries (reader : IO.TextReader) =
    reader.ReadToEnd().Split( LINE_SEPARATOR_CHARS )
    |> Array.map parseBoardFileEntry

let parseBoardFile (file : string) =
    use reader = new IO.StreamReader( file )
    parseBoardEntries reader

let unparseBoardFile (file : string) (board : BoardFileEntry[]) =
    use writer = new IO.StreamWriter( file )
    board
    |> Array.iter (unparseBoardFileEntry writer)

let mapHex f bfe =
    match bfe with
    | Hex hex -> Hex <| f hex
    | _ -> bfe

let iterHex f bfe =
    match bfe with
    | Hex hex -> f hex |> ignore
    | _ -> ()

type TerrainTable( terrains : Terrain list ) =
    let terrainTypes, terrainsByName, dupsRemoved =
        let terrainTypes = ResizeArray<string>()
        let terrainsByName = Dictionary<string, Terrain>()
        let dupsRemoved = ResizeArray<string>()
        for pte in terrains do
            if terrainsByName.ContainsKey( pte.Type ) then
                terrainsByName.[pte.Type] <- pte
                dupsRemoved.Add( pte.Type )
            else
                terrainTypes.Add( pte.Type )
                terrainsByName.[pte.Type] <- pte
        terrainTypes, terrainsByName, dupsRemoved

    new( srcHex : Hex ) = TerrainTable( srcHex.Terrains )

    new() = TerrainTable( List.empty )

    member this.TerrainTypes = terrainTypes :> seq<string>

    member this.TerrainEntries = terrainsByName.Values :> seq<Terrain>
    
    member this.DuplicatesRemoved = dupsRemoved :> seq<string>
    
    member this.Item
        with get tt = 
            terrainsByName.[tt]
        and set tt pte =
            if not <| terrainsByName.ContainsKey( tt ) then
                terrainTypes.Add( tt )
            terrainsByName.[tt] <- pte

    member this.TryGet( tt ) =
        let contains, pte = terrainsByName.TryGetValue( tt )
        if contains then Some pte else None

    member this.Contains( tt ) = 
        terrainsByName.ContainsKey( tt )

    member this.Add( te ) =
        if not <| terrainsByName.ContainsKey( te.Type ) then
            terrainTypes.Add( te.Type )
        terrainsByName.[te.Type] <- te

    member this.Remove( tt ) =
        terrainTypes.Remove( tt ) |> ignore
        terrainsByName.Remove( tt ) |> ignore

type Board =
    {
    XSize : int
    YSize : int
    Options : (string * string)[]
    Hexes : Hex[]
    }

let makeBoard (bfes : BoardFileEntry[]) =
    let mutable ok = true
    let mutable size = None
    let options = ResizeArray()
    let hexes = ResizeArray()
    
    for bfe in bfes do
        match bfe with
        | Size( xSize, ySize ) ->
            size <- Some( xSize, ySize )
        | Option( name, value ) ->
            options.Add( (name, value) )
        | Hex hex ->
            hexes.Add( hex )
        | End -> 
            ()
        | Unrecognized s ->
            ok <- ok && s.Length <= 0
    
    if ok && Option.isSome size then
        let xSize, ySize = Option.get size
        Some {
            XSize = xSize; YSize = ySize
            Options = options.ToArray()
            Hexes = hexes.ToArray()
        }
    else
        None

// NB: if there were any unrecognized lines discarded during Board creation, 
// they will be lost - so no round-trip. But who cares...
let unparseBoard (file : string) (board : Board) =
    let bfes = ResizeArray()
    bfes.Add( Size( board.XSize, board.YSize) )
    for optionName, optionValue in board.Options do
        bfes.Add( Option( optionName, optionValue ) )
    for hex in board.Hexes do
        bfes.Add( Hex hex )
    bfes.Add( End )
    unparseBoardFile file <| bfes.ToArray()
